package com.sinsz.pay.factory.wechat;

import com.sinsz.pay.dto.*;
import com.sinsz.pay.exception.PayException;
import com.sinsz.pay.factory.BasePay;
import com.sinsz.pay.factory.support.*;
import com.sinsz.pay.properties.PayProperties;
import com.sinsz.pay.support.Constant;
import com.sinsz.pay.support.WxPayConstant;
import com.sinsz.pay.util.PayUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.nutz.http.Http;
import org.nutz.http.Response;
import org.nutz.json.Json;
import org.nutz.json.JsonFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import static com.sinsz.pay.properties.support.WeChatPlatform.PUBLIC;

/**
 * 微信支付实现
 * <p>
 *     实现了app支付和公众号支付，暂不支持服务商和银行模式
 * </p>
 * @author chenjianbo
 */
public class WxPayImpl extends BasePay {

    private PayProperties prop;

    private HttpServletRequest request;

    private static final Logger logger = LoggerFactory.getLogger(WxPayImpl.class);

    private static final String RETURN_CODE = "return_code";
    private static final String RESULT_CODE = "result_code";

    public WxPayImpl(PayProperties prop,  HttpServletRequest request) {
        super(prop);
        this.prop = prop;
        this.request = request;
    }

    @Override
    public PayProperties getProp() {
        return prop;
    }

    @Override
    public void setProp(PayProperties prop) {
        this.prop = prop;
    }

    public HttpServletRequest getRequest() {
        return request;
    }

    public void setRequest(HttpServletRequest request) {
        this.request = request;
    }

    @Override
    public String unifiedOrder(String outTradeNo, String body, String detail, int fee, String openid) {
        //格式化基本参数
        outTradeNo = formatOutTradeNo(outTradeNo);
        body = formatBody(body);
        detail = formatDetail(body, detail);
        fee = formatFee(fee);
        openid = formatOpenid(openid);
        //处理支付时间与过期支付时间
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
        //支付开始时间
        long time = System.currentTimeMillis();
        String timeStart = format.format(new Date(time));
        //支付过期时间
        long endTime = time + WxPayConstant.ORDER_EXPIRATION_INTERVAL;
        String timeExpire = format.format(new Date(endTime));
        //参数组装并签名
        SortedMap<Object, Object> params = new TreeMap<>();
        params.put("version", WxPayConstant.VERSION);
        params.put("appid", prop.getWxpay().getAppid());
        params.put("mch_id", prop.getWxpay().getMchId());
        if (PUBLIC.equals(prop.getWxpay().getPlatform())) {
            params.put("device_info", WxPayConstant.DEVICE_INFO);
        }
        params.put("nonce_str", RandomStringUtils.randomNumeric(32).toUpperCase());
        params.put("body", body);
        params.put("detail", detail);
        if (!StringUtils.isEmpty(prop.getWxpay().getStoreId())
                && !StringUtils.isEmpty(prop.getWxpay().getStoreName())) {
            params.put("attach", "`store_appid="+
                    prop.getWxpay().getStoreId()+"#store_name="+
                    prop.getWxpay().getStoreName()+"#op_user="
            );
        }
        params.put("out_trade_no", outTradeNo);
        params.put("fee_type", WxPayConstant.FEE_TYPE);
        params.put("total_fee", fee);
        params.put("spbill_create_ip", request.getRemoteAddr());
        params.put("time_start", timeStart);
        params.put("time_expire", timeExpire);
        params.put("notify_url", PayUtils.redirect(prop.getHttp(), prop.getWxpay().getPayNotify(), false));
        params.put("trade_type", formatTradeType());
        params.put("limit_pay", WxPayConstant.LIMIT_PAY);
        if (PUBLIC.equals(prop.getWxpay().getPlatform())) {
            params.put("openid", openid);
        }
        params.put("sign_type", WxPayConstant.SIGN_TYPE);
        //参数签名
        params.put("sign", SignUtils.createSign(Charset.forName("UTF-8").name(), params, prop.getWxpay().getMchKey()).toUpperCase());
        //执行请求
        try {
            Response response = Http.postXML(WxPayConstant.UNIFIEDORDER, new Parser(params).mapToXml(),15000);
            if (!response.isOK()) {
                throw new PayException("微信支付网络或请求方式异常");
            }
            Map<String, Object> map = new Parser(response.getContent(Charset.forName("UTF-8").name())).xmlToMap();
            if (Constant.OK.equals(map.getOrDefault(RETURN_CODE, "").toString())
                    && Constant.OK.equals(map.getOrDefault(RESULT_CODE, "").toString())) {
                String prepayId = map.getOrDefault("prepay_id", "").toString();
                if (StringUtils.isEmpty(prepayId)) {
                    throw new PayException("获取支付订单凭据异常");
                }
                SortedMap<Object, Object> payInfo = new TreeMap<>();
                if (PUBLIC.equals(prop.getWxpay().getPlatform())) {
                    payInfo.put("appId", prop.getWxpay().getAppid());
                    payInfo.put("timeStamp", System.currentTimeMillis() / 1000 + "");
                    payInfo.put("nonceStr", RandomStringUtils.randomNumeric(32).toUpperCase());
                    payInfo.put("package", "prepay_id=" + prepayId);
                    payInfo.put("signType", "MD5");
                    payInfo.put("paySign",SignUtils.createSign(Charset.forName("UTF-8").name(), payInfo, prop.getWxpay().getMchKey()));
                } else {
                    payInfo.put("appid", prop.getWxpay().getAppid());
                    payInfo.put("partnerid", prop.getWxpay().getMchId());
                    payInfo.put("prepayid",  prepayId);
                    payInfo.put("package",   "Sign=WXPay");
                    payInfo.put("noncestr",  RandomStringUtils.randomNumeric(32).toUpperCase());
                    payInfo.put("timestamp", System.currentTimeMillis() / 1000 + "");
                    payInfo.put("sign", SignUtils.createSign(Charset.forName("UTF-8").name(), payInfo, prop.getWxpay().getMchKey()));
                }
                return Json.toJson(payInfo, JsonFormat.tidy());
            } else {
                throw new PayException(Json.toJson(map, JsonFormat.tidy()));
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new PayException("预支付创建未知异常.");
        }
    }

    @Override
    public String orderQuery(String transactionId, String outTradeNo) {
        outTradeNo = formatOutTradeNo(outTradeNo);
        //参数组装并签名
        SortedMap<Object, Object> params = new TreeMap<>();
        params.put("version", WxPayConstant.VERSION);
        params.put("appid", prop.getWxpay().getAppid());
        params.put("mch_id", prop.getWxpay().getMchId());
        if (!StringUtils.isEmpty(transactionId)) {
            params.put("transaction_id", transactionId);
        } else {
            params.put("out_trade_no", outTradeNo);
        }
        params.put("nonce_str", RandomStringUtils.randomNumeric(32).toUpperCase());
        params.put("sign_type", WxPayConstant.SIGN_TYPE);
        params.put("sign", SignUtils.createSign(Charset.forName("UTF-8").name(), params, prop.getWxpay().getMchKey()));
        //执行请求
        try {
            Response response = Http.postXML(WxPayConstant.ORDERQUERY, new Parser(params).mapToXml(),15000);
            if (!response.isOK()) {
                throw new PayException("微信支付网络或请求方式异常");
            }
            Map<String, Object> map = new Parser(response.getContent(Charset.forName("UTF-8").name())).xmlToMap();
            if (Constant.OK.equals(map.getOrDefault(RETURN_CODE, "").toString())
                    && Constant.OK.equals(map.getOrDefault(RESULT_CODE, "").toString())) {
                OrderQueryDto queryDto = new OrderQueryDto(
                        MergeStatus.wxPayStatus(map.getOrDefault("trade_state", "").toString()).name(),
                        formatMoney(Integer.valueOf(map.getOrDefault("total_fee", 0).toString())),
                        map.getOrDefault("transaction_id", "").toString(),
                        map.getOrDefault("out_trade_no", "").toString(),
                        map.getOrDefault("time_end", "").toString(),
                        map.getOrDefault("trade_state_desc", "").toString()
                );
                return Json.toJson(queryDto, JsonFormat.tidy());
            } else {
                throw new PayException(Json.toJson(map, JsonFormat.tidy()));
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new PayException("支付查询未知异常.");
        }
    }

    @Override
    public String closeOrder(String outTradeNo) {
        outTradeNo = formatOutTradeNo(outTradeNo);
        //参数组装并签名
        SortedMap<Object, Object> params = new TreeMap<>();
        params.put("appid", prop.getWxpay().getAppid());
        params.put("mch_id", prop.getWxpay().getMchId());
        params.put("out_trade_no", outTradeNo);
        params.put("nonce_str", RandomStringUtils.randomNumeric(32).toUpperCase());
        params.put("sign_type", WxPayConstant.SIGN_TYPE);
        params.put("sign", SignUtils.createSign(Charset.forName("UTF-8").name(), params, prop.getWxpay().getMchKey()));
        try {
            Response response = Http.postXML(WxPayConstant.CLOSEORDER, new Parser(params).mapToXml(),15000);
            if (!response.isOK()) {
                throw new PayException("微信支付网络或请求方式异常");
            }
            Map<String, Object> map = new Parser(response.getContent(Charset.forName("UTF-8").name())).xmlToMap();
            if (Constant.OK.equals(map.getOrDefault(RETURN_CODE, "").toString())
                    && Constant.OK.equals(map.getOrDefault(RESULT_CODE, "").toString())) {
                return Constant.OK;
            } else {
                logger.error("失败信息描述：" + Json.toJson(map, JsonFormat.tidy()));
                return Constant.FAIL;
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new PayException("支付关闭未知异常.");
        }
    }

    @Override
    public String refund(String transactionId, String outTradeNo, String outRefundNo, int totalFee, int fee, String refundDesc) {
        outTradeNo = formatOutTradeNo(outTradeNo);
        outRefundNo = formatOutRefundNo(outRefundNo);
        totalFee = formatFee(totalFee);
        fee = formatRefundFee(totalFee, fee);
        refundDesc = StringUtils.isEmpty(refundDesc) ? "退款": refundDesc;
        //参数组装并签名
        SortedMap<Object, Object> params = new TreeMap<>();
        params.put("appid", prop.getWxpay().getAppid());
        params.put("mch_id", prop.getWxpay().getMchId());
        params.put("nonce_str", RandomStringUtils.randomNumeric(32).toUpperCase());
        if (!StringUtils.isEmpty(transactionId)) {
            params.put("transaction_id", transactionId);
        } else {
            params.put("out_trade_no", outTradeNo);
        }
        params.put("out_refund_no", outRefundNo);
        params.put("total_fee", totalFee);
        params.put("refund_fee", fee);
        params.put("refund_fee_type", WxPayConstant.FEE_TYPE);
        params.put("refund_desc", refundDesc);
        params.put("refund_account", WxPayConstant.REFUND_ACCOUNT);
        if (!StringUtils.isEmpty(prop.getWxpay().getRefundNotify())) {
            params.put("notify_url", PayUtils.redirect(prop.getHttp(), prop.getWxpay().getRefundNotify(), false));
        }
        params.put("sign_type", WxPayConstant.SIGN_TYPE);
        params.put("sign", SignUtils.createSign(Charset.forName("UTF-8").name(), params, prop.getWxpay().getMchKey()));
        //执行请求
        try {
            Response response = SSLConstant.instance().postWxPay(WxPayConstant.REFUND, new Parser(params).mapToXml(),15000);
            if (!response.isOK()) {
                throw new PayException("微信支付网络或请求方式异常");
            }
            Map<String, Object> map = new Parser(response.getContent(Charset.forName("UTF-8").name())).xmlToMap();
            if (Constant.OK.equals(map.getOrDefault(RETURN_CODE, "").toString())
                    && Constant.OK.equals(map.getOrDefault(RESULT_CODE, "").toString())) {
                RefundDto refundDto = new RefundDto(
                        map.getOrDefault("transaction_id", "").toString(),
                        map.getOrDefault("out_trade_no", "").toString(),
                        map.getOrDefault("out_refund_no", "").toString(),
                        map.getOrDefault("refund_id", "").toString(),
                        System.currentTimeMillis()
                );
                return Json.toJson(refundDto, JsonFormat.tidy());
            } else {
                throw new PayException(Json.toJson(map, JsonFormat.tidy()));
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new PayException("支付退款申请未知异常.");
        }
    }

    @Override
    public String refundQuery(String refundId, String outRefundNo) {
        outRefundNo = formatOutRefundNo(outRefundNo);
        //参数组装并签名
        SortedMap<Object, Object> params = new TreeMap<>();
        params.put("appid", prop.getWxpay().getAppid());
        params.put("mch_id", prop.getWxpay().getMchId());
        if (!StringUtils.isEmpty(refundId)) {
            params.put("refund_id", refundId);
        } else {
            params.put("out_refund_no", outRefundNo);
        }
        params.put("nonce_str", RandomStringUtils.randomNumeric(32).toUpperCase());
        params.put("sign_type", WxPayConstant.SIGN_TYPE);
        params.put("sign", SignUtils.createSign(Charset.forName("UTF-8").name(), params, prop.getWxpay().getMchKey()));
        try {
            Response response = Http.postXML(WxPayConstant.REFUNDQUERY, new Parser(params).mapToXml(),15000);
            if (!response.isOK()) {
                throw new PayException("微信支付网络或请求方式异常");
            }
            Map<String, Object> map = new Parser(response.getContent(Charset.forName("UTF-8").name())).xmlToMap();
            if (Constant.OK.equals(map.getOrDefault(RETURN_CODE, "").toString())
                    && Constant.OK.equals(map.getOrDefault(RESULT_CODE, "").toString())) {
                RefundQueryDto refundQueryDto = new RefundQueryDto(
                        map.getOrDefault("transaction_id", "").toString(),
                        map.getOrDefault("out_trade_no", "").toString(),
                        "",
                        "",
                        Integer.valueOf(map.getOrDefault("refund_count", 0).toString()),
                        "",
                        ""
                );
                String rid = "refund_id",
                        orn = "out_refund_no",
                        refundStatus = "refund_status",
                        refundSuccessTime = "refund_success_time";
                map.forEach((key, value) -> {
                    if (key.contains(rid)) {
                        refundQueryDto.setRefundId(map.getOrDefault(key, "").toString());
                    }
                    if (key.contains(orn)) {
                        refundQueryDto.setOutRefundNo(map.getOrDefault(key, "").toString());
                    }
                    if (key.contains(refundStatus)) {
                        refundQueryDto.setRefundStatus(MergeStatus.wxRefundStatus(map.getOrDefault(key, "").toString()).name());
                    }
                    if (key.contains(refundSuccessTime)) {
                        refundQueryDto.setRefundSuccessTime(map.getOrDefault(key, "").toString());
                    }
                });
                return Json.toJson(refundQueryDto, JsonFormat.tidy());
            } else {
                throw new PayException(Json.toJson(map, JsonFormat.tidy()));
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new PayException("支付退款查询未知异常.");
        }
    }

    @Override
    public ResponseEntity<InputStreamResource> downloadBill(Date date, BillType billType) {
        String billDate = formatBillDate(date);
        billType = billType == null ? BillType.ALL: billType;
        switch (billType) {
            case TRADE:
            case SIGNCUSTOMER:
                billType = BillType.ALL;
                break;
            default:
        }
        //参数组装并签名
        SortedMap<Object, Object> params = new TreeMap<>();
        params.put("appid", prop.getWxpay().getAppid());
        params.put("mch_id", prop.getWxpay().getMchId());
        params.put("bill_date", billDate);
        params.put("bill_type", billType.name());
        params.put("tar_type", WxPayConstant.TAR_TYPE);
        params.put("nonce_str", RandomStringUtils.randomNumeric(32).toUpperCase());
        params.put("sign_type", WxPayConstant.SIGN_TYPE);
        params.put("sign", SignUtils.createSign(Charset.forName("UTF-8").name(), params, prop.getWxpay().getMchKey()));
        Response response = Http.postXML(WxPayConstant.DOWNLOADBILL, new Parser(params).mapToXml(),15000);
        if (!response.isOK()) {
            throw new PayException("微信支付网络或请求方式异常");
        }
        InputStream is = response.getStream();
        try{
            int len = is.available();
            if (len <= 149) {
                Map<String, Object> map = new Parser(response.getContent(Charset.forName("UTF-8").name())).xmlToMap();
                throw new PayException(Json.toJson(map, JsonFormat.tidy()));
            }
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setCacheControl("no-cache, no-store, must-revalidate");
            headers.setContentDispositionFormData("attachment", "wx" + billDate + ".gzip");
            headers.setPragma("no-cache");
            headers.setExpires(0L);
            return ResponseEntity
                    .ok()
                    .headers(headers)
                    .contentLength(len)
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .body(new InputStreamResource(is));
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new PayException("微信支付下载对账单异常");
        }
    }

    @Override
    public String payNotify() {
        try {
            String content = formatNotifyParams(request);
            if (StringUtils.isEmpty(content)) {
                throw new PayException("无回执支付参数");
            }
            Map<String, Object> map = new Parser(content).xmlToMap();
            if (Constant.OK.equals(map.getOrDefault(RETURN_CODE, "").toString())
                    && Constant.OK.equals(map.getOrDefault(RESULT_CODE, "").toString())) {
                OrderQueryDto queryDto = new OrderQueryDto(
                        MergeStatus.wxPayStatus(Constant.OK).name(),
                        formatMoney(Integer.valueOf(map.getOrDefault("total_fee", 0).toString())),
                        map.getOrDefault("transaction_id", "").toString(),
                        map.getOrDefault("out_trade_no", "").toString(),
                        map.getOrDefault("time_end", "").toString(),
                        "支付成功"
                );
                return Json.toJson(queryDto, JsonFormat.tidy());
            } else {
                throw new PayException(Json.toJson(map, JsonFormat.tidy()));
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new PayException("支付结果回调异常.");
        }
    }

    @Deprecated
    @Override
    public String refundNotify() {
        try {
            String content = formatNotifyParams(request);
            if (StringUtils.isEmpty(content)) {
                throw new PayException("无回执退款参数");
            }
            Map<String, Object> map = new Parser(content).xmlToMap();
            return Json.toJson(map, JsonFormat.tidy());
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new PayException("支付退款结果回调异常.");
        }
    }

    @Override
    public String transfers(String partnerTradeNo,String userName, int amount, String desc, String openid) {
        if (!PUBLIC.equals(prop.getWxpay().getPlatform())) {
            throw new PayException("请使用微信公众平台实现付款");
        }
        partnerTradeNo = formatPartnerTradeNo(partnerTradeNo);
        userName = formatUserName(userName);
        amount = formatTransferFee(amount);
        desc = StringUtils.isEmpty(desc) ? "平台转账" : desc;
        openid = formatOpenid(openid);
        //参数组装并签名
        SortedMap<Object, Object> params = new TreeMap<>();
        params.put("mch_appid", prop.getWxpay().getAppid());
        params.put("mchid", prop.getWxpay().getMchId());
        params.put("nonce_str", RandomStringUtils.randomNumeric(32).toUpperCase());
        params.put("partner_trade_no", partnerTradeNo);
        params.put("openid", openid);
        params.put("check_name", WxPayConstant.CHECK_NAME);
        params.put("re_user_name", userName);
        params.put("amount", amount);
        params.put("desc", desc);
        params.put("spbill_create_ip", request.getRemoteAddr());
        params.put("sign", SignUtils.createSign(Charset.forName("UTF-8").name(), params, prop.getWxpay().getMchKey()));
        try {
            Response response = SSLConstant.instance().postWxPay(WxPayConstant.TRANSFERS, new Parser(params).mapToXml(),15000);
            if (!response.isOK()) {
                throw new PayException("微信支付网络或请求方式异常");
            }
            Map<String, Object> map = new Parser(response.getContent(Charset.forName("UTF-8").name())).xmlToMap();
            if (Constant.OK.equals(map.getOrDefault(RETURN_CODE, "").toString())
                    && Constant.OK.equals(map.getOrDefault(RESULT_CODE, "").toString())) {
                TransferDto transferDto = new TransferDto();
                transferDto.setPartnerTradeNo(map.getOrDefault("partner_trade_no", "").toString());
                transferDto.setPaymentNo(map.getOrDefault("payment_no", "").toString());
                transferDto.setPaymentTime(map.getOrDefault("payment_time", "").toString());
                return Json.toJson(transferDto, JsonFormat.tidy());
            } else {
                throw new PayException(Json.toJson(map, JsonFormat.tidy()));
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new PayException("付款异常.");
        }
    }

    @Override
    public String getTransferInfo(String partnerTradeNo) {
        if (!PUBLIC.equals(prop.getWxpay().getPlatform())) {
            throw new PayException("请使用微信公众平台实现付款查询");
        }
        partnerTradeNo = formatPartnerTradeNo(partnerTradeNo);
        SortedMap<Object, Object> params = new TreeMap<>();
        params.put("appid", prop.getWxpay().getAppid());
        params.put("mch_id", prop.getWxpay().getMchId());
        params.put("nonce_str", RandomStringUtils.randomNumeric(32).toUpperCase());
        params.put("partner_trade_no", partnerTradeNo);
        params.put("sign", SignUtils.createSign(Charset.forName("UTF-8").name(), params, prop.getWxpay().getMchKey()));
        try {
            Response response = SSLConstant.instance().postWxPay(WxPayConstant.TRANSFERSQUERY, new Parser(params).mapToXml(),15000);
            if (!response.isOK()) {
                throw new PayException("微信支付网络或请求方式异常");
            }
            Map<String, Object> map = new Parser(response.getContent(Charset.forName("UTF-8").name())).xmlToMap();
            if (Constant.OK.equals(map.getOrDefault(RETURN_CODE, "").toString())
                    && Constant.OK.equals(map.getOrDefault(RESULT_CODE, "").toString())) {
                TransferQueryDto queryDto = new TransferQueryDto();
                queryDto.setPartnerTradeNo(map.getOrDefault("partner_trade_no", "").toString());
                queryDto.setPaymentNo(map.getOrDefault("detail_id", "").toString());
                queryDto.setStatus(MergeStatus.wxTransferStatus(map.getOrDefault("status", "").toString()).name());
                queryDto.setReason(map.getOrDefault("reason", "").toString());
                queryDto.setOpenid(map.getOrDefault("openid", "").toString());
                queryDto.setUserName(map.getOrDefault("transfer_name", "").toString());
                queryDto.setFee(formatMoney(Integer.valueOf(map.getOrDefault("payment_amount", 0).toString())));
                queryDto.setTransferTime(map.getOrDefault("transfer_time", "").toString());
                queryDto.setPaymentTime(map.getOrDefault("payment_time", "").toString());
                queryDto.setDesc(map.getOrDefault("desc", "").toString());
                return Json.toJson(queryDto, JsonFormat.tidy());
            } else {
                throw new PayException(Json.toJson(map, JsonFormat.tidy()));
            }
        } catch (Exception e) {
            e.printStackTrace(System.out);
            throw new PayException("查询付款异常.");
        }
    }

}